﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Monads;
using System.Reflection;
using System.Text;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Threading;

using ABCat.Shared.Properties;

using HtmlAgilityPack;

using Microsoft.Win32;

using Expression = System.Linq.Expressions.Expression;

using HtmlDocument = HtmlAgilityPack.HtmlDocument;

public static class Extensions
{
	#region + Fields +

	private const string Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890";

	private static readonly Random Rnd = new Random();
	private static readonly Dictionary<string, TimeSpan> TimeSpanParseCache = new Dictionary<string, TimeSpan>();

	#endregion
	#region + Logic +

	public static bool AnySafe<T>(this IEnumerable<T> value, Func<T, bool> predicate = null)
	{
		return value != null && (predicate == null ? value.Any() : value.Any(predicate));
	}

	public static string ChangeCase(this string text, CaseTypeEnum caseType, bool changeOnlyTargetChar, bool trim)
	{
		var sb = new StringBuilder(changeOnlyTargetChar ? (trim ? text.Trim(',', '.', ' ', '»', '«', '\"') : text) : (trim ? text.ToLower().Trim(',', '.', ' ', '»', '«', '\"') : text.ToLower()));

		if (sb.Length > 0)
		{
			if (trim)
			{
				int l;

				do
				{
					l = sb.Length;
					sb.Replace("  ", " ");
				} while (l > sb.Length);

				sb.Replace("|", "/");
				sb.Replace("\\", "/");
				sb.Replace("[", "(");
				sb.Replace("]", ")");

				InsertSpace(sb, ",", false, true);
				InsertSpace(sb, "/", true, true);
				InsertSpace(sb, ".", false, true);
				InsertSpace(sb, "(", true, false);
				InsertSpace(sb, ")", false, true);

				if (sb[0] == ' ') sb.Remove(0, 1); if (sb[sb.Length - 1] == ' ') sb.Remove(sb.Length - 1, 1);
			}
			sb[0] = sb[0].ToString(CultureInfo.InvariantCulture).ToUpper()[0];

			switch (caseType)
			{
				case CaseTypeEnum.AllWords:

					for (int z = 1; z < sb.Length; z++)
					{
						if (sb[z] != ' ' && sb[z - 1] == ' ')
						{
							sb[z] = sb[z].ToString(CultureInfo.InvariantCulture).ToUpper()[0];
						}
					}

					break;
				case CaseTypeEnum.AfterSplitter:
					bool splitter = false;

					for (int z = 1; z < sb.Length; z++)
					{
						if (sb[z] == '.' || sb[z] == ',' || sb[z] == '/' || sb[z] == '\\' || sb[z] == '*' || sb[z] == '|')
						{
							splitter = true;
						}
						else if (sb[z] != ' ' && splitter)
						{
							sb[z] = sb[z].ToString(CultureInfo.InvariantCulture).ToUpper()[0];
							splitter = false;
						}
					}

					break;
			}
		}

		return sb.ToString();
	}

	public static void CheckAccess(this Dispatcher dispatcher, Action action)
	{
		if (dispatcher.CheckAccess()) action();
		else dispatcher.BeginInvoke(action, null);
	}

	public static string CollectExceptionDetails(this Exception ex)
	{
		string result = null;
		result += "Message: {0}\r\nStack: {1}\r\n---------\r\n".F(ex.Message, ex.StackTrace);
		if (ex.InnerException != null) result += ex.InnerException.CollectExceptionDetails();
		return result;
	}

	public static bool CompareEnd<T>(this List<T> collection, List<T> search)
	{
		bool result = false;

		if (collection.Count >= search.Count)
		{
			result = true;
			for (int z = 0; z < search.Count; z++)
			{
				if (!Equals(collection[collection.Count - z - 1], search[search.Count - z - 1]))
				{
					result = false;
					break;
				}
			}
		}

		return result;
	}

	public static IEnumerable<HtmlNode> EnumerateAllNodes(this HtmlNode rootNode)
	{
		var nodes = new Queue<HtmlNode>();

		nodes.Enqueue(rootNode);

		while (nodes.Count > 0)
		{
			var node = nodes.Dequeue();
			yield return node;
			foreach (var childNode in node.ChildNodes) { nodes.Enqueue(childNode); }
		}
	}

	[StringFormatMethod("formatString")]
	public static string F(this string formatString, params object[] args)
	{
		return string.Format(formatString, args);
	}

	public static void Fire(this EventHandler eventHandler, object sender, EventArgs e = null)
	{
		if (eventHandler != null) eventHandler(sender, e ?? EventArgs.Empty);
	}

	public static void Fire<T>(this EventHandler<T> eventHandler, object sender, T e)
		where T : EventArgs
	{
		if (eventHandler != null) eventHandler(sender, e);
	}

	public static string GenerateRandomString(int size, int? seed = null)
	{
		var buffer = new char[size];

		var rnd = seed.HasValue ? new Random(seed.Value) : Rnd;

		for (int i = 0; i < size; i++)
		{
			buffer[i] = Chars[rnd.Next(Chars.Length)];
		}

		return new string(buffer);
	}

	public static string GetDescription(this Enum value)
	{
		FieldInfo field = value.GetType().GetField(value.ToString());
		DescriptionAttribute attribute = Attribute.GetCustomAttribute(field, typeof(DescriptionAttribute)) as DescriptionAttribute;
		return attribute == null ? value.ToString() : attribute.Description;
	}

	public static object GetEnumValueFromDescription(this string description, Type enumType)
	{
		if (!enumType.IsEnum) throw new InvalidOperationException();
		foreach (var field in enumType.GetFields())
		{
			var attribute = Attribute.GetCustomAttribute(field,
							typeof(DescriptionAttribute)) as DescriptionAttribute;
			if (attribute != null)
			{
				if (attribute.Description == description)
					return field.GetValue(null);
			}
			else
			{
				if (field.Name == description)
					return field.GetValue(null);
			}
		}
		throw new ArgumentException("Not found.", "description");
		// or return default(T);
	}

	[UsedImplicitly]
	public static IEnumerable<InstalledApplication> GetInstalledApplications()
	{
		var result = new List<InstalledApplication>();
		Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall").Do(item => result.AddRange(GetInstalledApplications(item)));
		Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall").Do(item => result.AddRange(GetInstalledApplications(item)));
		Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall").Do(item => result.AddRange(GetInstalledApplications(item)));
		return result;
	}

	public static IEnumerable<HtmlNode> GetNodes(this HtmlNode node, string nodeName, string attributeName, string attributeValue)
	{
		return node.Descendants(nodeName).Where(item => item.GetAttributeValue(attributeName, null) == attributeValue);
	}

	public static IEnumerable<HtmlNode> GetNodes(this HtmlNode node, string nodeName, string attributeName, Func<string, bool> predicate)
	{
		return node.Descendants(nodeName).Where(item => predicate(item.GetAttributeValue(attributeName, null)));
	}

	public static IEnumerable<HtmlNode> GetNodes(this HtmlDocument document, string nodeName, string attributeName, string attributeValue)
	{
		return document.DocumentNode.GetNodes(nodeName, attributeName, attributeValue);
	}

	public static IEnumerable<HtmlNode> GetNodes(this HtmlDocument document, string nodeName, string attributeName, Func<string, bool> predicate)
	{
		return document.DocumentNode.GetNodes(nodeName, attributeName, predicate);
	}

	public static IEnumerable<HtmlNode> GetNodesByClass(this HtmlNode node, string nodeName, string attributeValue)
	{
		return node.GetNodes(nodeName, "class", attributeValue);
	}

	public static IEnumerable<HtmlNode> GetNodesByClass(this HtmlDocument document, string nodeName, string attributeValue)
	{
		return document.GetNodes(nodeName, "class", attributeValue);
	}

	public static bool InheritsFrom(this Type t, Type baseType)
	{
		Type cur = t.BaseType;

		while (cur != null)
		{
			if (cur.Equals(baseType))
			{
				return true;
			}

			cur = cur.BaseType;
		}

		return false;
	}

	public static void InsertSpace(StringBuilder sb, string sign, bool spaceBefore, bool spaceAfter)
	{
		sb.Replace("{0} ".F(sign), sign);
		sb.Replace(" {0}".F(sign), sign);

		if (spaceBefore) sb.Replace(sign, " {0}".F(sign));
		if (spaceAfter) sb.Replace(sign, "{0} ".F(sign));
	}

	[ContractAnnotation("value:null => true")]
	public static bool IsNullOrEmpty([CanBeNull] this string value)
	{
		return string.IsNullOrEmpty(value);
	}

	public static void Pack(IEnumerable<string> sourceFiles, string topDirectory, string packFile)
	{
		if (File.Exists(packFile))
		{
			File.Delete(packFile);
		}

		using (var fs = new FileStream(packFile, FileMode.Create, FileAccess.Write))
		{
			using (var bw = new BinaryWriter(fs))
			{
				bw.Write("1.0");
				bw.Write("1.0");
				bw.Write(sourceFiles.Count());

				foreach (var sourceFile in sourceFiles)
				{
					var sourceContent = File.ReadAllBytes(sourceFile);
					bw.Write(sourceFile.Replace(topDirectory, ""));
					bw.Write(sourceContent.Length);
					bw.Write(sourceContent);
				}
			}
		}
	}

	public static int Randomize(this int value, int range)
	{
		return (value - range) + Rnd.Next(range * 2);
	}

	public static string[] Split(this string value, params string[] separators)
	{
		return value.Split(separators, StringSplitOptions.RemoveEmptyEntries);
	}

	public static byte[] ToBitmap(this FrameworkElement element)
	{
		var targetBitmap =
						new RenderTargetBitmap((int)element.ActualWidth,
																													(int)element.ActualHeight,
																													96d, 96d,
																													PixelFormats.Default);
		targetBitmap.Render(element);

		// add the RenderTargetBitmap to a Bitmapencoder
		var encoder = new BmpBitmapEncoder();
		encoder.Frames.Add(BitmapFrame.Create(targetBitmap));

		using (var ms = new MemoryStream())
		{
			encoder.Save(ms);
			return ms.ToArray();
		}
	}

	public static string ToStringOrEmpty(this object value)
	{
		return value == null ? string.Empty : value.ToString();
	}

	public static TimeSpan ToTimeSpan(this string lenght)
	{
		var result = TimeSpan.Zero;

		if (!lenght.IsNullOrEmpty() && !TimeSpanParseCache.TryGetValue(lenght, out result))
		{
			List<string> lenghts;

			if (!TimeSpan.TryParse(lenght, out result))
			{

				var lenght4Parse = lenght.ToLower().Replace(" ", "").Replace("примерно", "").Replace("около", "").Replace(",", "").Replace(".", "").Replace("~", "");

				var hours = new[] { "часа", "часов", "час.", "час", "ч.", "ч", "hours", "hour", "h" };
				var minutes = new[] { "минута", "минуты", "минут", "мин.", "мин", "м.", "м", "minutes", "min.", "min", "m" };
				var seconds = new[] { "секунда", "секунды", "секунд", "сек.", "сек", "с.", "с", "seconds", "sec.", "sec" };

				foreach (var hour in hours)
				{
					var lenght1 = lenght4Parse.Replace(hour, "*-*");
					if (lenght1 != lenght4Parse)
					{
						lenght4Parse = lenght1;
						//break;
					}
				}

				foreach (var minute in minutes)
				{
					var lenght1 = lenght4Parse.Replace(minute, "*--*");
					if (lenght1 != lenght4Parse)
					{
						lenght4Parse = lenght1;
						//break;
					}
				}

				foreach (var second in seconds)
				{
					var lenght1 = lenght4Parse.Replace(second, "*---*");
					if (lenght1 != lenght4Parse)
					{
						lenght4Parse = lenght1;
						//break;
					}
				}

				bool containsHours = lenght4Parse.Contains("*-*");
				bool containsMinutes = lenght4Parse.Contains("*--*");
				bool containsSeconds = lenght4Parse.Contains("*---*");

				if (containsHours || containsMinutes || containsSeconds)
				{
					lenght4Parse = lenght4Parse.Replace("*-*", ":");
					lenght4Parse = lenght4Parse.Replace("*--*", ":");
					lenght4Parse = lenght4Parse.Replace("*---*", ":");

					lenghts = new List<string>(lenght4Parse.Split(":"));

					if (lenghts.Count >= 2 && containsHours && containsMinutes && !containsSeconds)
					{
						lenghts.Insert(2, "00");
					}
					else if (lenghts.Count >= 1 && !containsHours && containsMinutes && !containsSeconds)
					{
						lenghts.Insert(0, "00");
						lenghts.Insert(2, "00");
					}
					else if (lenghts.Count >= 1 && containsHours && !containsMinutes && !containsSeconds)
					{
						lenghts.Insert(1, "00");
						lenghts.Insert(2, "00");
					}
					else if (!containsHours && containsMinutes && containsSeconds)
					{
						lenghts.Insert(0, "00");
					}
					else if (lenghts.Count >= 2 && !containsHours && containsMinutes)
					{
						lenghts.Insert(0, "00");
						lenghts.Insert(2, "00");
					}
				}
				else
				{
					lenghts = new List<string>(lenght4Parse.Split(":"));
				}
			}
			else
			{
				lenghts = new List<string>(lenght.Split(":"));
			}

			if (lenghts.Count == 2) lenghts.Insert(0, "00");

			if (lenghts.Count >= 3)
			{
				Int16 hour = 0;
				Int16 minute = 0;
				Int16 second = 0;

				if (lenghts.Count > 0 && !Int16.TryParse(lenghts[0], out hour))
				{
					hour = 0;
				}
				else if (lenghts.Count > 1 && !Int16.TryParse(lenghts[1], out minute))
				{
					minute = 0;
				}
				else if (lenghts.Count > 2 && !Int16.TryParse(lenghts[2], out second))
				{
					second = 0;
				}

				//if (Int32.TryParse(lenghts[0], out hour) && Int32.TryParse(lenghts[1], out minute) && Int32.TryParse(lenghts[2], out second))
				{
					result = new TimeSpan(hour, minute, second);
				}
			}

			TimeSpanParseCache[lenght] = result;
		}

		return result;
	}

	public static void UnPack(string packFile, string targetPath, Action<int> reportProgressCallBack)
	{
		using (var fs = new FileStream(packFile, FileMode.Open, FileAccess.Read))
		{
			using (var br = new BinaryReader(fs))
			{
				var version = br.ReadString();
				var softVersion = br.ReadString();

				if (version == "1.0")
				{
					try
					{
						int filesCount = br.ReadInt32();

						for (int z = 0; z < filesCount; z++)
						{
							string filePathName = Path.Combine(targetPath, br.ReadString());

							var lenght = br.ReadInt32();
							try
							{
								File.WriteAllBytes(filePathName, br.ReadBytes(lenght));
								reportProgressCallBack((int)(fs.Position / (double)fs.Length) * 100);
							}
							catch (Exception ex)
							{
								throw new Exception("Ошибка создания файла '{0}'".F(filePathName), ex);
							}
						}
					}
					catch (Exception ex)
					{
						throw new Exception("Ошибка чтения пакета '{0}'".F(packFile), ex);
					}
				}
				else
				{
					throw new Exception("Невозможно открыть пакет. Требуется версия программы не ниже '{0}'. Версия пакета: '{1}'.".F(softVersion, version));
				}
			}
		}
	}

	private static IEnumerable<InstalledApplication> GetInstalledApplications([NotNull] RegistryKey key)
	{
		var result = new List<InstalledApplication>();
		foreach (String keyName in key.GetSubKeyNames())
		{
			key.OpenSubKey(keyName).Do(subKey =>
				{
					var displayName = subKey.GetValue("DisplayName").ToStringOrEmpty();
					result.Add(new InstalledApplication() { DisplayName = displayName, DisplayVersion = subKey.GetValue("DisplayVersion").ToStringOrEmpty(), InstallLocation = subKey.GetValue("InstallLocation").ToStringOrEmpty() });
				});
		}
		return result;
	}

	#endregion
	#region # Enums #

	public enum CaseTypeEnum
	{
		FirstWord,
		AllWords,
		AfterSplitter
	}

	#endregion
	#region # Nested Classes #

	public class InstalledApplication
	{
		#region + Properties +

		public string DisplayName		{get; set;}
		public string DisplayVersion		{get; set;}
		public string ExePath		{get; set;}
		public string InstallLocation		{get; set;}

		#endregion
		#region + Logic +

		public bool CheckExists()
		{
			return !string.IsNullOrEmpty(InstallLocation) && File.Exists(InstallLocation);
		}

		#endregion
	}

	#endregion
}